package com.yumo.kangchenjunga.coupon.v1;

import java.io.IOException;
import java.io.InputStream;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Random;
import java.util.Set;
import java.util.stream.Collectors;

import javax.transaction.Transactional;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.ExampleMatcher;
import org.springframework.data.domain.ExampleMatcher.StringMatcher;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;

import com.google.common.collect.Iterables;
import com.yumo.kangchenjunga.activity.Activity;
import com.yumo.kangchenjunga.activity.ActivityService;
import com.yumo.kangchenjunga.activity.NoSuchActivityException;
import com.yumo.kangchenjunga.customer.Customer;
import com.yumo.kangchenjunga.customer.CustomerService;
import com.yumo.kangchenjunga.log.LogEntity;
import com.yumo.kangchenjunga.log.LogService;
import com.yumo.kangchenjunga.security.CustomUserDetails;
import com.yumo.kangchenjunga.user.NoSuchUserException;
import com.yumo.kangchenjunga.user.UserService;

import cn.leafcraft.utils.webutil.datetime.DateTimeUtils;

@Service
public class CouponService {

	@Autowired
	private CouponRepository repository;
	@Autowired
	private CouponLogRepository logRepository;
	@Autowired
	private ActivityService activityService;
	@Autowired
	private CustomerService customerService;
	@Autowired
	private UserService userService;
	@Autowired
	private CouponPoolRepository poolRepository;
	@Autowired
	private CouponCustomerAllocationRepository allocationRepository;
	@Autowired
	private LogService logService;

	@Autowired
	private com.yumo.kangchenjunga.coupon.v2.CouponService couponService2;

	private Random random = new Random();

	public CouponEntity queryById(int id) throws NoSuchCouponException {
		return repository.findById(id).orElseThrow(NoSuchCouponException::new);
	}

	public Iterable<CouponEntity> list() {
		return repository.findAll();
	}

	public Page<CouponEntity> page(Pageable pageable) {
		return repository.findAll(pageable);
	}

	public int generate(int activityId, int amount, int termByDay) throws NoSuchActivityException {
		Activity activity = activityService.queryById(activityId);
		Instant now = DateTimeUtils.instantNow();

		Objects.requireNonNull(activity);

		var existsCouponList = poolRepository.findByActivity(activity);
		var existsCouponCode = existsCouponList.stream().map(it -> it.couponCode).collect(Collectors.toSet());
		var newCouponList = doGenerate(existsCouponCode, amount);
		var newCouponEntityList = new ArrayList<CouponPoolEntity>();

		for (int i = 0; i < newCouponList.size(); ++i) {
			CouponPoolEntity entity = new CouponPoolEntity();

			entity.activity = activity;
			entity.couponCode = newCouponList.get(i);
			entity.createTime = now;
			entity.createType = CreateType.GENERATED;
			entity.effectiveTime = null;
			entity.expirationTime = null;
			entity.secretCode = genSecret(6);
			entity.status = CouponStatus.UNALLOCATED;
			entity.termByMinute = termByDay * 1440;

			newCouponEntityList.add(entity);
		}

		logService.log(genLog(activity, "生成", "成功",
				String.format("生成优惠券成功 权益ID: %d, 权益标题: '%s', 共 %d 张", activity.getId(), activity.title, amount)));

		return Iterables.size(poolRepository.saveAll(newCouponEntityList));
	}

	private String genSecret(int len) {
		long n = random.nextInt(1_000_000);

		return prettySecret(n);
	}

	private List<String> doGenerate(Set<String> existsCouponCode, int amount) {
		var randomStream = new Random().longs(0, 100_000_000L);

		return randomStream.mapToObj(this::prettyCoupon).filter(s -> !existsCouponCode.contains(s))
				.limit(amount).collect(Collectors.toList());
	}

	private String prettyCoupon(long l) {
		return pretty(l, 8);
	}

	private String prettySecret(long l) {
		return pretty(l, 6);
	}

	private String pretty(long l, int len) {
		int[] map = { '6', '8', '9' };
		var str = String.valueOf(l);

		str = str.codePoints().map(it -> (it == '4') ? map[random.nextInt(3)] : it)
				.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append).toString();

		return StringUtils.leftPad(str, len, '0');
	}

	// FIXME: 使用前验证优惠券的有效性
	public List<CouponLogEntity> redeem(List<String> couponCodes, String opStore, String remark, int opUserId)
			throws NoSuchUserException {
		var codes = repository.findByCouponCodeIn(couponCodes);
		var user = userService.queryById(opUserId);
		var logs = codes.stream()
				.map(it -> new CouponLogEntity(it.activity.getId(), it.activity.title, it.customer.getId(),
						it.customer.customerNumber, it.activity.company.getId(), it.activity.company.name,
						it.couponCode, "redeem", opUserId, user.username, opStore, "success", "", it.getId(), remark))
				.collect(Collectors.toList());

		codes.forEach(it -> it.status = Status.USED);

		repository.saveAll(codes);
		logRepository.saveAll(logs);

		return logs;
	}

	public VerifyCouponVo verify(String couponCode, String opStore, int companyId, int opUserId)
			throws NoSuchUserException {
		var code = repository.findByCouponCodeAndStatus(couponCode, Status.VALID);
		var user = userService.queryById(opUserId);

		return code.map(e -> doVerify(e, couponCode, opStore, companyId, opUserId, user.username))
				.orElseGet(() -> invalidCoupon(couponCode, opStore));
	}

	private VerifyCouponVo doVerify(CouponEntity coupon, String couponCode, String opStore, int companyId,
			int opUserId, String opUsername) {
		var now = DateTimeUtils.localDateNow();

		if (coupon.activity.company.getId() == companyId
				&& (now.isEqual(coupon.effectiveTime) || now.isAfter(coupon.effectiveTime))
				&& (now.isEqual(coupon.expirationTime) || now.isBefore(coupon.expirationTime))) {
			var verifyLog = new CouponLogEntity(coupon.activity.getId(), coupon.activity.title, coupon.customer.getId(),
					coupon.customer.customerNumber, coupon.activity.company.getId(), coupon.activity.company.name,
					couponCode, "verify", opUserId, opUsername, opStore,
					"success", "", coupon.getId(), "");
			var list = repository.findByActivityAndCustomerAndStatusAndCouponCodeNot(coupon.activity, coupon.customer,
					Status.VALID, coupon.couponCode);
			var others = list.stream()
					.map(it -> new CouponLogEntity(coupon.activity.getId(), coupon.activity.title,
							coupon.customer.getId(), coupon.customer.customerNumber, coupon.activity.company.getId(),
							coupon.activity.company.name, it.couponCode, "verify", opUserId, opUsername, opStore,
							"success", "", it.getId(), ""))
					.collect(Collectors.toList());

			logRepository.save(verifyLog);
			logRepository.saveAll(others);

			return new VerifyCouponVo(verifyLog, others);
		}

		return invalidCoupon(couponCode, opStore);
	}

	private VerifyCouponVo invalidCoupon(String couponCode, String opStore) {
		var log = new CouponLogEntity(0, "", 0, "", 0, "", couponCode, "verify", 0, "", opStore, "failure", "", 0, "");

		logRepository.save(log);

		return new VerifyCouponVo(log, null);
	}

	public boolean existsCoupon(Activity activity) {
		return poolRepository.existsByActivity(activity);
	}

	public boolean notExistsCoupon(Activity activity) {
		return !existsCoupon(activity);
	}

	public void interrupt(Activity activity) {
		repository.updateCouponStatusFromValidToIgnoredByActivityId(activity);
	}

	public Page<CouponEntity> query(CouponEntity coupon, Pageable pageable) {
		ExampleMatcher matcher = ExampleMatcher.matchingAll().withIgnoreCase()
				.withIgnorePaths("id", "activity.customerLevels", "activity.status", "customer.id")
				.withStringMatcher(StringMatcher.CONTAINING);
		Example<CouponEntity> example = Example.of(coupon, matcher);

		return repository.findAll(example, pageable);
	}

	@Transactional
	public ImportResult importCoupon(InputStream input, Activity activity) throws IOException {

		try (Workbook workbook = WorkbookFactory.create(input)) {
			var sheet = workbook.getSheetAt(0);
			var now = DateTimeUtils.instantNow();
			var errorDetails = new ArrayList<ImportErrorDetailItem>();

			for (int i = 1; i <= sheet.getLastRowNum(); ++i) {
				Row row = sheet.getRow(i);
				int importActivityId = Integer.parseInt(getCellAsString(row.getCell(0)));
				String couponCode = getCellAsString(row.getCell(2));
				String secretCode = getCellAsString(row.getCell(3));
				int importTerm = Integer.parseInt(getCellAsString(row.getCell(4)));

				if (importActivityId != activity.getId()) {
					errorDetails.add(
							new ImportErrorDetailItem("导入文件中权益ID与选择的权益不符。", i, 0, String.valueOf(importActivityId)));

					continue;
				}

				CouponPoolEntity entity = new CouponPoolEntity();

				entity.activity = activity;
				entity.couponCode = couponCode;
				entity.createTime = now;
				entity.createType = CreateType.IMPORTED;
				entity.effectiveTime = null;
				entity.expirationTime = null;
				entity.secretCode = secretCode;
				entity.status = CouponStatus.UNALLOCATED;
				entity.termByMinute = importTerm * 1440;

				try {
					poolRepository.save(entity);
				} catch (Exception e) {
					errorDetails.add(new ImportErrorDetailItem(e.getMessage(), i, -1, couponCode));
				}
			}

			if (CollectionUtils.isNotEmpty(errorDetails)) {
				logService.log(genLog(activity, "导入", "失败",
						String.format("导入优惠券失败 权益ID: %d, 权益标题: '%s'", activity.getId(), activity.title)));

				throw new ImportFailedException(new ImportResult("failed", errorDetails));
			}

			logService.log(genLog(activity, "导入", "成功",
					String.format("导入优惠券成功 权益ID: %d, 权益标题: '%s', 共 %d 张", activity.getId(), activity.title,
							sheet.getLastRowNum())));

			return new ImportResult("successful", null);
		}
	}

	private String getCellAsString(Cell cell) {
		switch (cell.getCellType()) {
		case STRING:
			return cell.getStringCellValue();

		case NUMERIC:
			return Integer.toString((int) cell.getNumericCellValue());

		default:
			return "";
		}
	}

	@Transactional
	public AllocateResultVo allocateByCustomerLevel(int activityId, int customerLevel, int perCustomer)
			throws NoSuchActivityException {
		var customerList = customerService.getCustomerByLevel(customerLevel);
		var activity = activityService.findById(activityId);
		var result = allocate(activityId, perCustomer, customerList);

		if (result.couponAmount == 0) {
			logService.log(genLog(activity, "发放", "失败", String.format(
					"按信托客户等级发放优惠券失败，剩余优惠券数量不足 权益ID: %d, 权益标题: '%s', 信托客户等级: %d, 共 %d 信托客户, 每信托客户 %d 张, 共发放 %d 张",
					activity.getId(), activity.title, customerLevel, result.customerAmount,
					result.couponPerCustomer, result.couponAmount)));
		} else {
			logService.log(genLog(activity, "发放", "成功",
					String.format("按信托客户等级发放优惠券成功 权益ID: %d, 权益标题: '%s', 信托客户等级: %d, 共 %d 信托客户, 每信托客户 %d 张, 共发放 %d 张",
							activity.getId(), activity.title, customerLevel, result.customerAmount,
							result.couponPerCustomer, result.couponAmount)));
		}

		return result;
	}

	@Transactional
	public AllocateResultVo allocateByCustomer(int activityId, List<Integer> customerIds, int perCustomer)
			throws NoSuchActivityException {
		var customerList = customerService.getCustomeByIds(customerIds);
		var activity = activityService.findById(activityId);
		var result = allocate(activityId, perCustomer, customerList);

		if (result.couponAmount == 0) {
			logService.log(genLog(activity, "发放", "失败",
					String.format("按信托客户发放优惠券失败，剩余优惠券数量不足 权益ID: %d, 权益标题: '%s', 共 %d 信托客户, 每信托客户 %d 张, 共发放 %d 张",
							activity.getId(), activity.title, result.customerAmount, result.couponPerCustomer,
							result.couponAmount)));
		} else {
			logService.log(genLog(activity, "发放", "成功",
					String.format("按信托客户发放优惠券成功 权益ID: %d, 权益标题: '%s', 共 %d 信托客户, 每信托客户 %d 张, 共发放 %d 张",
							activity.getId(), activity.title, result.customerAmount, result.couponPerCustomer,
							result.couponAmount)));
		}

		return result;
	}

	private synchronized AllocateResultVo allocate(int activityId, int perCustomer, List<Customer> customerList) {
		if (CollectionUtils.isNotEmpty(customerList)) {
			Instant now = DateTimeUtils.instantNow();
			int needCouponAmount = customerList.size() * perCustomer;
			long couponAmount = poolRepository.countByActivityIdAndStatus(activityId, CouponStatus.UNALLOCATED);

			if (couponAmount >= needCouponAmount) {
				var couponList = poolRepository.findByActivityIdAndStatus(activityId, CouponStatus.UNALLOCATED,
						PageRequest.of(0, needCouponAmount));
				var allocateList = new ArrayList<CouponCustomerAllocationEntity>(needCouponAmount);

				if (couponList.size() == needCouponAmount) {
					for (int i = 0; i < couponList.size(); ++i) {
						CouponPoolEntity pool = couponList.get(i);

						pool.status = CouponStatus.ALLOCATED;

						CouponCustomerAllocationEntity allocation = new CouponCustomerAllocationEntity();

						allocation.activity = pool.activity;
						allocation.allocateTime = now;
						allocation.couponCode = pool.couponCode;
						allocation.couponId = pool.getId();
						allocation.customer = customerList.get(i / perCustomer);
						allocation.effectiveTime = null;
						allocation.expirationTime = null;
						allocation.secretCode = pool.secretCode;
						allocation.status = CouponStatus.ALLOCATED;
						allocation.termByMinute = pool.termByMinute;

						allocateList.add(allocation);
					}

					poolRepository.saveAll(couponList);
					allocationRepository.saveAll(allocateList);

					customerList.forEach(it -> {
						for (int i = 0; i < perCustomer; ++i) {
							try {
								couponService2.obtain(it.getId(), activityId);
							} catch (NoMoreAllocatedCouponException | NoSuchActivityException e) {
								throw new RuntimeException();
							}
						}
					});
				}

				return new AllocateResultVo(customerList.size(), perCustomer, needCouponAmount);
			}
		}

		return new AllocateResultVo(customerList.size(), perCustomer, 0);
	}

	public CouponCustomerSummaryVo getCouponSummaryByCustomerId(int customerId) {
		var list = allocationRepository.summaryByCustomerId(customerId);
		var details = list.stream()
				.map(it -> new CouponCustomerActivitySummaryVo(it.getCustomerId(), it.getActivityId(),
						it.getCoupons(), it.getObtainedCoupons(), it.getUsedCoupons()))
				.collect(Collectors.toList());
		int totalCoupons = details.stream().mapToInt(it -> it.coupons).sum();
		int totalObtainedCoupons = details.stream().mapToInt(it -> it.obtainedCoupons).sum();
		int totalUsedCoupons = details.stream().mapToInt(it -> it.usedCoupons).sum();
		int totalActivity = details.size();

		return new CouponCustomerSummaryVo(customerId, totalCoupons, totalObtainedCoupons, totalUsedCoupons,
				totalActivity, details);
	}

	private LogEntity genLog(Activity activity, String op, String result, String detail) {
		var user = (CustomUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
		var log = new LogEntity();

		log.activityId = activity.getId();
		log.activityTitle = activity.title;
		log.companyId = activity.company.getId();
		log.companyName = activity.company.name;
		log.op = op;
		log.opDetail = detail;
		log.opObject = "优惠券";
		log.opResult = result;
		log.opUserId = user.getId();
		log.opUsername = user.getUsername();

		return log;
	}

}
